package ru.aslanov.schedule.server.gcalendar;

import com.google.gdata.data.ExtensionProfile;
import com.google.gdata.data.calendar.CalendarEventEntry;
import com.google.gdata.data.calendar.CalendarEventFeed;
import com.google.gdata.data.extensions.Recurrence;
import com.google.gdata.data.extensions.Where;
import ru.aslanov.schedule.model.*;
import ru.aslanov.schedule.shared.DayOfWeek;
import ru.aslanov.schedule.utils.XMLUtil;

import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import java.io.*;
import java.text.DateFormatSymbols;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Created by IntelliJ IDEA.
 * Created: Nov 30, 2009 6:50:57 PM
 *
 * @author Sergey Aslanov
 */
public class GCalParser {
    private static final Logger log = Logger.getLogger(GCalParser.class.getName());

    private Schedule existingSchedule;
    private static final Pattern TAG_PATTERN = Pattern.compile("<[^>]*>");
    private static final Pattern WHITESPACE_PATTERN = Pattern.compile("\\s{2,}", Pattern.MULTILINE | Pattern.DOTALL);

    public GCalParser(Schedule existingSchedule) {
        this.existingSchedule = existingSchedule;
    }

    public GCalParser() {        
    }

    public List<Group> parseSchedule(Reader gCalReader, Date now) throws Exception {
        CalendarEventFeed calendarEventFeed = new CalendarEventFeed();
        final ExtensionProfile extensionProfile = new ExtensionProfile();
        calendarEventFeed.declareExtensions(extensionProfile);
        calendarEventFeed.parseAtom(extensionProfile, gCalReader);

        return parseSchedule(calendarEventFeed, now);
    }

    public List<Group> parseSchedule(CalendarEventFeed eventFeed, Date now) throws Exception {
        final DateFormatSymbols formatSymbols = new DateFormatSymbols(new Locale("ru"));
        formatSymbols.setMonths(new String[]{"января", "февраля", "марта", "апреля", "мая", "июня", "июля", "августа", "сентября", "октября", "ноября", "декабря"});
        SimpleDateFormat dateFormat = new SimpleDateFormat("dd MMMMM yyyy", formatSymbols);
        SimpleDateFormat untilDateFormat = new SimpleDateFormat("yyyyMMddHHmm");

        final Pattern reccurPattern = Pattern.compile("\\s*DTSTART;TZID=Europe/Moscow:(\\d{8})T(\\d\\d)(\\d\\d)00" +
                "\\s*DTEND;TZID=Europe/Moscow:\\d{8}T(\\d\\d)(\\d\\d)00" +
                "\\s*RRULE:FREQ=WEEKLY;.*?BYDAY=([^;]*);WKST=MO(;UNTIL=(\\d{8})T(\\d{6})Z)?", Pattern.MULTILINE | Pattern.DOTALL);

        Pattern hrefPattern = Pattern.compile("<a\\s*href=\"([^\"]+)\">\\s*([^<]+)\\s*</a>[\\s\\.]*([^<]*)", Pattern.MULTILINE | Pattern.DOTALL);
        Pattern sectionPattern = Pattern.compile("<b>\\s*([^<]*?)\\s*:\\s*</b>\\s*(.*?)\\s*", Pattern.MULTILINE | Pattern.DOTALL);

        List<Group> groups = new ArrayList<Group>();

        Calendar calendar = Calendar.getInstance();
        calendar.setTime(now);

        final List<CalendarEventEntry> entries = eventFeed.getEntries();
        for (CalendarEventEntry entry : entries) {
            String gId = entry.getId();
            String eventTitle = entry.getTitle().getPlainText();
            String content = entry.getPlainTextContent();
            final List<Where> locations = entry.getLocations();
            String where = locations.size() > 0 ? locations.get(0).getValueString() : null;
            Recurrence recurrence = entry.getRecurrence();

            String startDateRecStr;
            String startHH;
            String startMM;
            String endHH;
            String endMM;
            String days;
            String until;
            Date untilDate = null;
            boolean privates = false;
            if (recurrence == null) {
                log.fine("Reccurence is not defined for " + eventTitle + ", skipping");
                continue;
            } else {
                final Matcher matcher = reccurPattern.matcher(recurrence.getValue());
                if (matcher.find()) {
                    startDateRecStr = matcher.group(1);
                    startHH = matcher.group(2);
                    startMM = matcher.group(3);
                    endHH = matcher.group(4);
                    endMM = matcher.group(5);
                    days = matcher.group(6);
                    until = matcher.group(8);

                    Date startDate = untilDateFormat.parse(startDateRecStr + "0000");
                    if (startDate.after(now)) {
                        log.fine("Start date is in future: " + startDateRecStr);
                        //continue;
                    }
                    if (until != null) {
                        log.fine("Until date = " + until);
                        try {
                            untilDate = untilDateFormat.parse(until + "2359");
                        } catch (ParseException e) {
                            log.warning("Cannot parse until date: " + until);
                            throw e;
                        }
                    }
                } else {
                    throw new RuntimeException("Invalid reccurence string: " + recurrence);
                    //log.warning("Invalid reccurence string: " + recurrence);
                }
            }

            String title = null;
            Dance dance = null;
            Level level = null;

            if (eventTitle.contains("Частные уроки") || eventTitle.contains("Самоподготовка")) {
                privates = true;
                title = eventTitle;
            } else {
                String danceStr;
                String levelStr;
                final int levelIndex = eventTitle.indexOf(' ');
                if (levelIndex > 0) {
                    danceStr = clearTags(eventTitle.substring(0, levelIndex));
                    levelStr = clearTags(eventTitle.substring(levelIndex + 1));
                    if (levelStr.equals("Beginners")) {
                        level = getLevel("beg");
                    } else if (levelStr.equals("Juniors")) {
                        level = getLevel("kids");
                    } else if (levelStr.equals("Juniors Beginners")) {
                        level = getLevel("kids_beg");
                    } else if (levelStr.equals("Intermediate")) {
                        level = getLevel("int");
                    } else if (levelStr.equals("Intermediate - II") || levelStr.equals("Intermediate - I")) {
                        level = getLevel("int");
                        title = levelStr;
                    } else if (levelStr.equals("Advanced")) {
                        level = getLevel("adv");
                    } else if (levelStr.equals("Comp & Show")) {
                        level = getLevel("cs");
                    } else if (levelStr.equals("Show \"Энергия вот отсюда...\"")) {
                        level = getLevel("cs");
                        title = levelStr;
                    } else {
                        danceStr = eventTitle;
                    }
                } else {
                    danceStr = eventTitle;
                }

                if (danceStr.equals("LH")) {
                    dance = getDance("lh");
                } else if (danceStr.equals("BW")) {
                    dance = getDance("bw");
                } else if (danceStr.equals("Shag")) {
                    dance = getDance("shag");
                } else if (danceStr.equals("Couple & Solo Blues")) {
                    dance = getDance("blues");
                } else if (danceStr.equals("BS")) {
                    dance = getDance("bal");
                } else if (danceStr.equals("CH and Solo Jazz")) {
                    dance = getDance("ch_jazz");
                } else if (danceStr.equals("CH")) {
                    dance = getDance("ch");
                } else if (danceStr.equals("Solo Jazz")) {
                    dance = getDance("jazz");
                } else if (danceStr.equals("Jitterbug (Буги + линди)")) {
                    dance = getDance("jitter");
                } else {
                    throw new RuntimeException("Don't know dance: " + danceStr);
                }
            }

            final String[] lines = content.split("<br/>|<br>");
            //final String[] sections = content.split("<b>|</b>");
            boolean first = true;
            String group = null;
            String[] teachers = null;
            String teacher = null;
            String address = null;
            Date startDate = null;
            String starts = null;
            String welcome = null;
            StringBuilder comment = new StringBuilder();
            String teachersUrl = null;
            for (int j = 0; j < lines.length; j++) {
                String line = lines[j].trim();
                if (line.length() == 0) continue;
                
                final Matcher sectMatcher = sectionPattern.matcher(line);
                if (sectMatcher.matches()) {
                    String secName = sectMatcher.group(1);
                    String secValue = sectMatcher.group(2);

                    if (secName.equals("Группа")) {
                        group = secValue.trim();
                    } else if (secName.equals("Преподаватели") || secName.equals("Преподаватель")) {
                        String teachersStr = normalizeSpaces(secValue);
                        final Matcher matcher = hrefPattern.matcher(teachersStr);
                        if (matcher.matches()) {
                            teachersUrl = matcher.group(1);
                            teachersStr = matcher.group(2);
                            comment.append(matcher.group(3)).append(" ");
                        }
                        teachers = teachersStr.split(" и ");
                    } else if (secName.equals("Как проехать")) {
                        address = normalizeSpaces(secValue);
                    } else if (secName.equals("Старт группы")) {
                        starts = clearTags(secValue);
                        try {
                            startDate = dateFormat.parse(starts);
                        } catch (ParseException e) {
                            startDate = dateFormat.parse(starts + " " + (calendar.get(Calendar.YEAR)));
                        }
                    } else if (secName.equals("Прием в группу")) {
                        welcome = normalizeSpaces(secValue);
                    } else if (secName.equals("Стоимость")) {
                        comment.append("<b>" + secName + ":</b> ").append(secValue).append(" ");
                    } else {
                        throw new Exception("Unrecognizable group: " + secName);
                        //log.warning("Unrecognizable group: " + secName);
                    }
                } else {
                    if (first) {
                        comment.append(line).append(" ");
                    } else if (j == lines.length - 1) {
                        comment.append(normalizeSpaces(line)).append(" ");
                    } else {
                        log.warning("Unrecognizable line: " + line);
                    }
                    first = false;
                }
            }
            Teacher[] teachersArray = null;
            if (teachers != null) {
                if (teachers.length > 2) {
                    throw new RuntimeException("Can't have more than 2 teachers");
                }
                teachersArray = new Teacher[teachers.length];
                int t = 0;
                for (String teacherStr : teachers) {
                    final Matcher matcher = hrefPattern.matcher(teacherStr);
                    String teacherName0 = null;
                    String teacherUrl0 = null;
                    if (matcher.matches()) {
                        teacherUrl0 = matcher.group(1);
                        teacherName0 = matcher.group(2);
                        comment.append(matcher.group(3)).append(" ");
                    } else {
                        teacherName0 = teacherStr;
                        teacherUrl0 = teachersUrl;

                    }
                    log.fine("Teacher " + teacherName0 + " (" + teacherUrl0 + ") - " + comment);
                    teachersArray[t++] = getTeacher(teacherName0);
                }
            }
            String commentStr = comment.toString();
            commentStr = commentStr.trim();
            if (commentStr.length() == 0) commentStr = null;

            Teacher teacher1 = teachersArray != null && teachersArray.length > 0 ? teachersArray[0] : null;
            Teacher teacher2 = teachersArray != null && teachersArray.length > 1 ? teachersArray[1] : null;

            final String[] daysOfWeek = days.split(",");
            for (String dayOfWeek : daysOfWeek) {
                DayOfWeek dayOfWeekInt = getDayOfWeek(dayOfWeek);
                Date lastClassDate = null;
                if (untilDate != null) {
                    lastClassDate = calculateLastClassDate(untilDate, dayOfWeekInt);
                    if (lastClassDate.before(now)) {
                        log.fine("Until date is in the past: " + until);
                        //continue;
                    }
                }

                ScheduleItem scheduleItem = new ScheduleItem(dayOfWeekInt, getTime(startHH, startMM), getTime(endHH, endMM),
                        getLocationByAddress(address, where), gId);
                SortedSet<ScheduleItem> scheduleItems = new TreeSet<ScheduleItem>();
                scheduleItems.add(scheduleItem);

                Group groupObj = new Group(dance, level, startDate,
                        teacher1, teacher2,
                        commentStr, false,
                        null, new ArrayList<ScheduleItem>(scheduleItems),
                        privates ? Boolean.TRUE : null, title, welcome, lastClassDate);
                groups.add(groupObj);
            }
        }

        collapseSchedule(groups, now);

        return groups;
    }

    private Dance getDance(String danceId) {
        for (Dance dance : existingSchedule.getDances()) {
            if (danceId.equals(dance.getId())) {
                return dance;
            }
        }
        throw new RuntimeException("Existing schedule doesn't have dance " + danceId);
    }

    private Level getLevel(String levelId) {
        for (Level level : existingSchedule.getLevels()) {
            if (levelId.equals(level.getId())) {
                return level;
            }
        }
        throw new RuntimeException("Existing schedule doesn't have level " + levelId);
    }

    private Location getLocationByAddress(String address, String where) {
        for (Location location : existingSchedule.getLocations()) {
            if (address.contains(location.getAddress()) ||
                    (where != null && normalizeAddress(where).equals(normalizeAddress(location.getAddress())))) {
                return location;
            }
        }
        throw new RuntimeException("Existing schedule doesn't have location '" + address + "' (" + where + ")");

        //return getLocation(locId);
    }

    private static String normalizeAddress(String address) {
        String res = address.replaceAll("ул\\.|Москва|,", " ");
        res = normalizeSpaces(res);
        return res;
    }

    private Location getLocation(String locId) {
        for (Location location : existingSchedule.getLocations()) {
            if (locId.equalsIgnoreCase(location.getId())) {
                return location;
            }
        }
        throw new RuntimeException("Existing schedule doesn't have location " + locId);
    }

    private String getTime(String hours, String minutes) {
        return hours + ":" + minutes;
    }

    private DayOfWeek getDayOfWeek(String dayOfWeek) {
        if ("MO".equals(dayOfWeek)) {
            return DayOfWeek.mon;
        } else if ("TU".equals(dayOfWeek)) {
            return DayOfWeek.tue;
        } else if ("WE".equals(dayOfWeek)) {
            return DayOfWeek.wed;
        } else if ("TH".equals(dayOfWeek)) {
            return DayOfWeek.thu;
        } else if ("FR".equals(dayOfWeek)) {
            return DayOfWeek.fri;
        } else if ("SA".equals(dayOfWeek)) {
            return DayOfWeek.sat;
        } else if ("SU".equals(dayOfWeek)) {
            return DayOfWeek.sun;
        } else {
            throw new RuntimeException("Invalid week day: " + dayOfWeek);
        }
    }

    private Teacher getTeacher(String teacherName) {
        for (Teacher teacher : existingSchedule.getTeachers()) {
            if (teacherName.equalsIgnoreCase(teacher.getName())) {
                return teacher;
            }
        }
        throw new RuntimeException("Existing schedule doesn't have teacher " + teacherName);
    }



    private String clearTags(String section) {
        String res = TAG_PATTERN.matcher(section).replaceAll(" ");
        res = normalizeSpaces(res);
        return res;
    }

    private static String normalizeSpaces(String res) {
        return WHITESPACE_PATTERN.matcher(res).replaceAll(" ").trim();
    }

    private void loadExistingSchedule(File file) throws Exception {
        final Unmarshaller unmarshaller = XMLUtil.JAXB_CONTEXT.createUnmarshaller();
        existingSchedule = (Schedule) unmarshaller.unmarshal(file);
    }

    private void exportToXml(Schedule schedule, File file) throws Exception {
        Marshaller m = XMLUtil.JAXB_CONTEXT.createMarshaller();
        m.marshal(schedule, file);
    }

    private void collapseSchedule(List<Group> groupList, Date now) {
        for (int i = 0; i < groupList.size(); i++) {
            Group g1 = groupList.get(i);
            for (int j = groupList.size() - 1; j >= i + 1; j--) {
                Group g2 = groupList.get(j);

                if (g1.getDance() == g2.getDance() && g1.getLevel() == g2.getLevel()
                        && g1.getTeacher1() == g2.getTeacher1() && g1.getTeacher2() == g2.getTeacher2()
                        && ((g1.getTitle() == null && g2.getTitle() == null) || (g1.getTitle() != null && g1.getTitle().equals(g2.getTitle())))
                        && g1.getScheduleItems().iterator().next().getLocation() == g2.getScheduleItems().iterator().next().getLocation()
                        && ((g1.getStarted() == null && g2.getStarted() == null) || (g1.getStarted() != null && g1.getStarted().equals(g2.getStarted())))
                        && ((g1.getUntil() == null && g2.getUntil() == null) || (g1.getUntil() != null && g2.getUntil() != null))) {
                    g1.getScheduleItems().addAll(g2.getScheduleItems());
                    if (g2.getUntil() != null) {
                        if (g1.getUntil() == null || g2.getUntil().after(g1.getUntil())) {
                            g1.setUntil(g2.getUntil());
                        }
                    }
                    groupList.remove(j);
                }
            }
        }

        for (Iterator<Group> groupIterator = groupList.iterator(); groupIterator.hasNext();) {
            Group group = groupIterator.next();

            if (group.getUntil() != null && group.getUntil().before(now)) {
                groupIterator.remove();
                continue;
            }
            if (group.getStarted() != null && group.getStarted().after(now)) {
                groupIterator.remove();
                continue;
            }

            Location loc = null;
            for (ScheduleItem scheduleItem : group.getScheduleItems()) {
                if (loc == null) {
                    loc = scheduleItem.getLocation();
                } else {
                    if (loc != scheduleItem.getLocation()) {
                        loc = null;
                        break;
                    }
                }
            }
            if (loc != null) {
                group.setLocation(loc);
                for (ScheduleItem scheduleItem : group.getScheduleItems()) {
                    scheduleItem.setLocation(null);
                }
            }

        }
    }

    private Date calculateLastClassDate(Date untilDate, DayOfWeek dayOfWeek) {
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(untilDate);

        int calWeekDay = (dayOfWeek.ordinal() + 1) % 7 + 1;

        while (calendar.get(Calendar.DAY_OF_WEEK) != calWeekDay) {
            calendar.add(Calendar.DATE, -1);
        }

        return calendar.getTime();
    }


    private void createSchedule(File[] calendarDataFiles, File existingScheduleFile, File outFile, Date now) throws Exception {
        loadExistingSchedule(existingScheduleFile);

        Schedule wholeSchedule = new Schedule();
        for (File file : calendarDataFiles) {
            log.info("parsing file " + file.getName());
            final Reader reader = new InputStreamReader(
                    new FileInputStream(file), "UTF-8");
            final List<Group> groups = parseSchedule(reader, now);

            wholeSchedule.getGroups().addAll(groups);

            reader.close();
        }

        wholeSchedule.setTeachers(existingSchedule.getTeachers());
        wholeSchedule.setLocations(existingSchedule.getLocations());
        wholeSchedule.setDances(existingSchedule.getDances());
        wholeSchedule.setLevels(existingSchedule.getLevels());

        exportToXml(wholeSchedule, outFile);
    }


    public static void main(String[] args) throws Exception {
        final File existingScheduleFile = new File("data/schedule.xml");
        final File outFile = new File("out/schedule_from_gcal.xml");

        File dir = new File("out/from_gcal");
        final File[] calendarDataFiles = dir.listFiles(new FilenameFilter() {
            @Override
            public boolean accept(File dir, String name) {
                return name.endsWith(".xml");
            }
        });
        Calendar calendar = Calendar.getInstance();
        //calendar.clear();
        //calendar.set(2010, 0, 28);

        final Date now = calendar.getTime();
        //final Date now = new Date();

        GCalParser gCalParser = new GCalParser();
        gCalParser.createSchedule(calendarDataFiles, existingScheduleFile, outFile, now);
    }

}
